package com.yaDuo.common.util

import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.os.Process
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.lang.Thread.UncaughtExceptionHandler
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess


/**
 * @author YaDuo
 * time 2025-01-24 14:48
 * 监听应用Crash
 * 会打出日志并将此次错误日志保存在本地目录下
 */
@SuppressLint("StaticFieldLeak")
object CrashReportHandler : UncaughtExceptionHandler {

    private const val TAG = "CrashCommonHandler"

    /** 保存的文件名 **/
    private const val FILE_NAME = "CrashReport"

    /** 保存的文件格式后缀 **/
    private const val FILE_NAME_SUFFIX = ".log"

    /**
     * 保存的文件路径
     * 从 Android 6.0（API 23）开始，需要动态请求存储权限。
     * 在 Android 10 及以上版本，由于分区存储限制，推荐使用 Context.getExternalFilesDir()
     * 这种方式无需权限，目录位于：
     * /storage/emulated/0/Android/data/com.yaDuo.ticketUnion/files/
     */
    private var CRASH_FILE_PATH: String? = null

    /** 最大支持保存的文件数 **/
    private const val CRASH_FILE_COUNT_LIMIT = 10

    /** 系统默认的异常处理器 **/
    private val DEFAULT_CRASH_HANDLER = Thread.getDefaultUncaughtExceptionHandler()

    private var sContext: Context? = null


    /**
     * 初始化全局异常处理器
     * @param context 上下文
     */
    fun init(context: Context?) {
        requireNotNull(context) {
            "Context cannot be null" // 检查参数合法性
        }
        sContext = context.applicationContext // 使用全局上下文，避免内存泄漏
        CRASH_FILE_PATH = "${
            sContext!!.getExternalFilesDir(null)?.absolutePath
        }/${sContext!!.packageName}/"
        Thread.setDefaultUncaughtExceptionHandler(this) // 设置当前类为默认异常处理器
        LogUtil.d(TAG, "init success")
    }

    /**
     * 当程序中有未被捕获的异常，系统将会自动调用[uncaughtException]
     * @param p0 出现未捕获异常的线程
     * @param p1 未捕获的异常,有这个就可以得到异常信息
     */
    override fun uncaughtException(p0: Thread, p1: Throwable) {

        // 如果是调试器异常，直接返回
        if (isDebuggerException(p1)) {
            LogUtil.i(TAG, "is debugger exception, return")
            return
        }

        // 导出异常信息到文件
        dumpExceptionToSDCard(p1)

        // 打印异常日志
        LogUtil.e(TAG, "Uncaught exception in thread:  ${p0.name}, $p1")

        // 使用系统默认异常处理器或直接终止进程
        if (DEFAULT_CRASH_HANDLER != null) {
            DEFAULT_CRASH_HANDLER.uncaughtException(p0, p1) // 调用默认处理器
        } else {
            Process.killProcess(Process.myPid()) // 终止当前进程
            exitProcess(0)
        }
    }

    /**
     * 判断是否为调试器相关的异常
     * @param ex 异常对象
     * @return 是否为调试器异常
     */
    private fun isDebuggerException(ex: Throwable): Boolean {
        return ex.stackTrace.any {
            it.className == "dalvik.system.VMDebug" || it.className == "android.os.Debug"
        }
    }

    /**
     * 导出异常信息到文件
     * @param ex 异常对象
     */
    private fun dumpExceptionToSDCard(ex: Throwable) {

        val dir = CRASH_FILE_PATH?.let { File(it) } // 创建日志目录

        if(dir == null) {
            LogUtil.e(TAG, "dumpExceptionToSDCard dir == null")
            return
        }

        if (!dir.exists() && !dir.mkdirs()) { // 如果目录不存在且创建失败
            LogUtil.e(TAG, "Failed to create directory: $CRASH_FILE_PATH")
            return
        }

        checkAndDeleteOldFiles(dir)

        // 创建日志文件
        val timestamp: String =
            SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault()).format(Date())
        val file =
            File(dir, FILE_NAME + "_" + sContext!!.packageName + "_" + timestamp + FILE_NAME_SUFFIX)

        runCatching {
            PrintWriter(BufferedWriter(FileWriter(file))).use { pw ->
                // 写入崩溃时间
                pw.println(
                    "Crash Time: " + SimpleDateFormat(
                        "yyyy-MM-dd HH:mm:ss",
                        Locale.getDefault()
                    ).format(Date())
                )
                dumpDeviceInfo(pw)
                // 写入异常堆栈信息
                pw.println()
                ex.printStackTrace(pw) // 写入异常堆栈信息
            }
        }.onFailure {
            LogUtil.e(TAG, "Failed to dump crash info to file $it") // 捕获写入异常并打印日志
        }
    }

    /**
     * 写入设备信息到日志文件
     * @param pw 打印流
     * 将会依次写入应用包名、系统版本、设备制造商、设备型号
     */
    @Throws(NameNotFoundException::class)
    private fun dumpDeviceInfo(pw: PrintWriter) {
        pw.println("App Package: ${sContext!!.packageName}")
        pw.println("OS Version: ${Build.VERSION.RELEASE} + ${Build.VERSION.SDK_INT}")
        pw.println("Manufacturer: ${Build.MANUFACTURER}")
        pw.println("Model: ${Build.MODEL}")
    }

    /**
     * 检查并删除旧的CrashReport文件
     * @param dir 日志目录
     */
    private fun checkAndDeleteOldFiles(dir: File) {
        val files = dir.listFiles { _: File?, name: String ->
            name.endsWith(
                FILE_NAME_SUFFIX
            )
        } // 筛选日志文件
        if (files != null && files.size > CRASH_FILE_COUNT_LIMIT) { // 如果文件数量超过限制
            val earliestFile = findEarliestFile(files) // 找到最早的文件
            if (!earliestFile.delete()) { // 删除文件失败时打印警告
                LogUtil.w(TAG, "Failed to delete old crash file: " + earliestFile.name)
            }
        }
    }

    /**
     * 找到最早的日志文件
     * @param files 日志文件数组
     */
    private fun findEarliestFile(files: Array<File>): File {
        var earliestFile = files[0]
        for (file in files) {
            if (file.lastModified() < earliestFile.lastModified()) {
                earliestFile = file
                LogUtil.d(TAG, "findEarliestFile earliestFile: ${earliestFile.name}")
            }
        }
        return earliestFile
    }

}